package com.tools.web.http;

import com.tools.common.container.MapEnum;
import com.tools.common.exception.DataGenerationException;
import com.tools.common.exception.IORuntimeException;
import com.tools.common.io.CharsetEnum;
import com.tools.common.object.json.JsonKit;
import com.tools.common.object.Note;
import com.tools.common.object.ObjKit;
import com.tools.common.object.string.StrKit;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * HTTP 操作的工具类
 * */
@Note("HTTP 操作的工具类")
public final class HttpKit {

    private HttpKit() {}

    @Note("同步请求用的 HTTP 客户端对象")
    private static volatile CloseableHttpClient CLIENT;

    @Note("异步请求用的 HTTP 客户端对象")
    private static volatile CloseableHttpAsyncClient ASYNC_CLIENT;

    @Note("日志输出对象")
    private static volatile Logger LOGGER;

    @Note("本对象作为字符串打印输出时，其内容的上下边框")
    private static final String LOG_PRINT_BORDER = StrKit.addString("##", 100);

    /* **************************************************************************************
     *
     *          构建请求客户端
     *
     * **************************************************************************************
     * */


    @Note("构建默认的请求配置对象，并自动填充一些默认参数")
    public static RequestConfig buildDefaultRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(3000)
                .setConnectionRequestTimeout(5000)
                .setSocketTimeout(10000)
                .setRedirectsEnabled(true)
                .build();
    }


    @Note("构建同步请求的 HTTP 客户端对象，并设置绕过 SSL 验证处理 HTTPS 的请求")
    public static CloseableHttpClient build() {
        SSLConnectionSocketFactory sslFactory = private_newIgnoreVerifySSLForSyncClient();
        return HttpClients.custom()
                .setSSLSocketFactory(sslFactory)
                .setDefaultRequestConfig(buildDefaultRequestConfig())
                .build();
    }


    @Note("构建异步请求的 HTTP 客户端对象，并设置绕过 SSL 验证处理 HTTPS 的请求")
    public static CloseableHttpAsyncClient buildAsync() {
        SSLIOSessionStrategy sslStrategy = private_newIgnoreVerifySSLForAsyncClient();
        return HttpAsyncClients.custom()
                .setSSLStrategy(sslStrategy)
                .setDefaultRequestConfig(buildDefaultRequestConfig())
                .build();
    }


    /* **************************************************************************************
     *
     *          请求方法
     *
     * **************************************************************************************
     * */


    @Note("【成品方法】: 发送 GET 请求，获取 HTTP 结果的包装类对象")
    public static HttpResult sendGet(HttpBuilder httpBuilder) {
        private_httpBuilderCheck(httpBuilder);
        String parameter = buildGetParameters(httpBuilder.getParameters());
        String url = httpBuilder.getUrl();
        RequestConfig requestConfig = httpBuilder.getRequestConfig();
        HttpGet httpGet = new HttpGet(url + parameter);
        httpGet.setConfig(requestConfig);
        httpGet.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
        httpGet.setHeader("Request-Method", "GET");
        putAllHeadersToHttpEntity(httpGet, httpBuilder.getHeaders());
        if(httpBuilder.isAsync()) {
            private_asyncExecute(httpGet, httpBuilder, "GET");
            return null;
        }
        return private_execute(httpGet, httpBuilder, "GET");
    }


    @Note("【成品方法】: 发送 POST 请求，获取 HTTP 结果的包装类对象")
    public static HttpResult sendPost(HttpBuilder httpBuilder) {
        private_httpBuilderCheck(httpBuilder);
        HttpPost httpPost = new HttpPost(httpBuilder.getUrl());
        HttpEntity entity = buildUploadFilesAndPostParameters(httpBuilder.getParameters(), httpBuilder.getUploadFiles());
        httpPost.setHeader("Request-Method", "POST");
        putAllHeadersToHttpEntity(httpPost, httpBuilder.getHeaders());
        httpPost.setEntity(entity);
        httpPost.setConfig(httpBuilder.getRequestConfig());
        if(httpBuilder.isAsync()) {
            private_asyncExecute(httpPost, httpBuilder, "POST");
            return null;
        }
        return private_execute(httpPost, httpBuilder, "POST");
    }



    @Note("【成品方法】: 发送 PUT 请求，获取 HTTP 结果的包装类对象")
    public static HttpResult sendPut(HttpBuilder httpBuilder) {
        private_httpBuilderCheck(httpBuilder);
        HttpPut httpPut = new HttpPut(httpBuilder.getUrl());
        httpPut.setConfig(httpBuilder.getRequestConfig());
        httpPut.setHeader("Content-type", "application/json");
        httpPut.setHeader("DataEncoding", "UTF-8");
        httpPut.setHeader("Request-Method", "PUT");
        putAllHeadersToHttpEntity(httpPut, httpBuilder.getHeaders());
        StringEntity entity = buildPutParameters(httpBuilder.getParameters());
        httpPut.setEntity(entity);
        if(httpBuilder.isAsync()) {
            private_asyncExecute(httpPut, httpBuilder, "PUT");
            return null;
        }
        return private_execute(httpPut, httpBuilder, "PUT");
    }


    @Note("【成品方法】: 发送 DELETE 请求，获取 HTTP 结果的包装类对象")
    public static HttpResult sendDelete(HttpBuilder httpBuilder) {
        private_httpBuilderCheck(httpBuilder);
        String url = httpBuilder.getUrl();
        String paramString = buildGetParameters(httpBuilder.getParameters());
        HttpDelete httpDelete = new HttpDelete(url + paramString);
        httpDelete.setConfig(httpBuilder.getRequestConfig());
        httpDelete.setHeader("Content-type", "application/json");
        httpDelete.setHeader("Data-Encoding", "UTF-8");
        httpDelete.setHeader("Request-Method", "DELETE");
        putAllHeadersToHttpEntity(httpDelete, httpBuilder.getHeaders());
        if(httpBuilder.isAsync()) {
            private_asyncExecute(httpDelete, httpBuilder, "DELETE");
            return null;
        }
        return private_execute(httpDelete, httpBuilder, "DELETE");
    }

    /* **************************************************************************************
     *
     *          其他方法
     *
     * **************************************************************************************
     * */


    @Note("构建 GET 请求的参数字符串，即 URL 的 ? 后的参数字符串")
    public static String buildGetParameters(Map<String, Object> parameters) {
        if(parameters == null || parameters.isEmpty()) return "";
        StringBuilder builder = new StringBuilder("?");
        for(Map.Entry<String, Object> e : parameters.entrySet()) {
            String key = e.getKey();
            Object value = e.getValue();
            builder.append(key)
                    .append("=")
                    .append(value == null ? "" : value)
                    .append("&");
        }
        int length = builder.length();
        if(length == 0) return "";
        return builder.substring(0, length - 1);
    }


    @Note("把请求参数全部转换成 POST 请求所需的键值对参数体列表返回")
    public static List<NameValuePair> buildPostParameters(Map<String, Object> parameters) {
        if(parameters == null || parameters.isEmpty()) {
            return new ArrayList<>(0);
        }
        List<NameValuePair> pairList = new ArrayList<>(parameters.size());
        for(Map.Entry<String, Object> e : parameters.entrySet()) {
            NameValuePair pair = new BasicNameValuePair(e.getKey(), ObjKit.toStringValue(e.getValue(), "", true));
            pairList.add(pair);
        }
        return pairList;
    }


    @Note("把请求参数全部转换成 PUT 请求所需的键值对参数体列表返回")
    public static StringEntity buildPutParameters(Map<String, Object> parameters) {
        if(parameters == null || parameters.isEmpty()) {
            return new StringEntity("", StandardCharsets.UTF_8);
        }
        String paramJson = JsonKit.toJSON(parameters);
        return new StringEntity(paramJson, StandardCharsets.UTF_8);
    }


    @Note("把头文件一一设置进 HTTP 请求对象 httpRequest 里")
    public static void putAllHeadersToHttpEntity(HttpUriRequest httpRequest, Map<String, String> headers) {
        if(httpRequest == null || headers == null || headers.isEmpty()) return;
        for(Map.Entry<String, String> e : headers.entrySet()) {
            httpRequest.setHeader(e.getKey(), e.getValue());
        }
    }



    @Note("把所有上传文件和请求参数进行装载，构建成一个 HttpEntity 对象。上传文件和请求参数可以一起上传，也可以择一上传，另一个可以为空")
    public static HttpEntity buildUploadFilesAndPostParameters(Map<String, Object> parameters, Set<UploadFile> uploadFileSet) {
        if(uploadFileSet == null || uploadFileSet.isEmpty()) {
            List<NameValuePair> pairList = buildPostParameters(parameters);
            return new UrlEncodedFormEntity(pairList, StandardCharsets.UTF_8);
        }
        MultipartEntityBuilder builder = MultipartEntityBuilder.create()
                .setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        for(UploadFile file : uploadFileSet) {
            builder.addBinaryBody(file.getUploadKey(), file.getInputStream(),
                    ContentType.create("multipart/form-data"), file.getFilename());
        }
        if(parameters == null || parameters.isEmpty()) {
            return builder.build();
        }
        for(Map.Entry<String, Object> e : parameters.entrySet()) {
            String valueString = ObjKit.toStringValue(e.getValue(), "", true);
            builder.addPart(e.getKey(), new StringBody(valueString, ContentType.MULTIPART_FORM_DATA));
        }
        return builder.build();
    }

    /* ***************************************************************************************
     *
     *      请求结果处理的静态工具方法
     *
     * **************************************************************************************
     * */

    @Note("获取所有响应头，封装到一个 Map 集合里返回")
    public static Map<String, String> responseHeaderMap(HttpResponse response) {
        if(response == null) return new HashMap<>();
        Header[] headers = response.getAllHeaders();
        if(headers == null || headers.length == 0) return new HashMap<>();
        Map<String, String> result = MapEnum.HASH.newMap(headers.length);
        for (Header h : headers) {
            if(h != null) result.put(h.getName(), h.getValue());
        }
        return result;
    }

    @Note("获取响应报文的数据，以字节输入流的形式返回")
    public static InputStream responseBodyInputStream(HttpEntity httpEntity) {
        if(httpEntity == null) return null;
        try {
            return httpEntity.getContent();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("获取响应报文的数据，以报文默认的返回的字符集编码成字符串的形式返回")
    public static String responseBodyString(HttpEntity httpEntity) {
        return responseBodyString(httpEntity, null);
    }


    @Note("获取响应报文的数据，以指定字符集编码成字符串的形式返回")
    public static String responseBodyString(HttpEntity httpEntity, CharsetEnum charset) {
        if(httpEntity == null) return "";
        try {
            return (charset == null)
                    ? EntityUtils.toString(httpEntity)
                    : EntityUtils.toString(httpEntity, charset.toCharset());
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    /* **************************************************************************************
     *
     *          关闭客户端连接
     *
     * **************************************************************************************
     * */


    @Note("关闭指定的 HTTP 客户端对象")
    public static void close(CloseableHttpClient client) {
        if(client != null) {
            try {
                client.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
        }
    }


    @Note("关闭指定的异步 HTTP 客户端对象")
    public static void close(CloseableHttpAsyncClient client) {
        if(client != null) {
            try {
                client.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
        }
    }


    @Note("关闭 HTTP 请求后的响应对象")
    public static void close(CloseableHttpResponse response) {
        if(response != null) {
            try {
                response.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
        }
    }

    /* *******************************************************************************************
     *
     *          私有逻辑
     *
     * *******************************************************************************************
     * */

    @Note("懒加载同步请求客户端")
    private static void private_lazyInitClient() {
        if(CLIENT != null) return;
        synchronized (HttpKit.class) {
            if(CLIENT != null) return;
            CLIENT = build();
        }
    }

    @Note("懒加载异步请求客户端")
    private static void private_lazyInitAsyncClient() {
        if(ASYNC_CLIENT != null) return;
        synchronized (HttpKit.class) {
            if(ASYNC_CLIENT != null) return;
            ASYNC_CLIENT = buildAsync();
        }
    }


    @Note("懒加载日志输出对象")
    private static void private_lazyInitLogger() {
        if(LOGGER != null) return;
        synchronized (HttpKit.class) {
            if(LOGGER != null) return;
            LOGGER = LoggerFactory.getLogger(HttpKit.class);
        }
    }

    @Note("验证 HTTP 构建对象，检查一些必须参数")
    private static void private_httpBuilderCheck(HttpBuilder builder) {
        if(builder == null) throw new IllegalArgumentException("发送 HTTP 请求必须的构建对象不能为 null");
        String url = builder.getUrl();
        if(url == null || url.isEmpty()) throw new IllegalArgumentException("发送 HTTP 请求必须的 URL 不可以为空");
        if(builder.getRequestConfig() == null) builder.setRequestConfig(buildDefaultRequestConfig());
    }



    @Note("发送同步请求")
    private static HttpResult private_execute(HttpUriRequest httpRequest, HttpBuilder http, String method) {
        HttpResult result;
        try {
            private_lazyInitClient();
            CloseableHttpResponse response = CLIENT.execute(httpRequest);
            result = new HttpResult(response);
            http.setHttpResult(result);
            private_lazyInitLogger();
            String info = http.isPrintLog()
                    ? "发送 HTTP 请求执行成功，详细请求信息如下：\n" + private_outputHttpBuilderInfo(http, method)
                    : "发送 HTTP 请求执行成功";
            LOGGER.info(info);
        } catch (IOException e) {
            private_lazyInitLogger();
            String info = http.isPrintLog()
                    ? "发送 HTTP 请求执行过程失败，详细请求信息如下：\n" + private_outputHttpBuilderInfo(http, method)
                    : "发送 HTTP 请求执行过程失败";
            LOGGER.error(info);
            throw new IORuntimeException(e);
        }
        return result;
    }


    @Note("发送异步请求")
    private static void private_asyncExecute(HttpUriRequest httpRequest, HttpBuilder http, String method) {
        try {
            private_lazyInitAsyncClient();
            ASYNC_CLIENT.start();
            ASYNC_CLIENT.execute(httpRequest, new AsyncCallbackFun(http.getAsyncCallback(), http));
            private_lazyInitLogger();
            String info = http.isPrintLog()
                    ? "发送异步的 HTTP 请求执行成功，详细请求信息如下：\n" + private_outputHttpBuilderInfo(http, method)
                    : "发送异步的 HTTP 请求执行成功";
            LOGGER.info(info);
        } catch (Exception e) {
            private_lazyInitLogger();
            String info = http.isPrintLog()
                    ? "发送异步的 HTTP 请求执行过程失败，详细请求信息如下：\n" + private_outputHttpBuilderInfo(http, method)
                    : "发送异步的 HTTP 请求执行过程失败";
            LOGGER.error(info);
            throw new IORuntimeException(e);
        }
    }


    @Note("输出 HTTP 请求的所有配置项参数信息")
    private static String private_outputHttpBuilderInfo(HttpBuilder http, String method) {
        return LOG_PRINT_BORDER +
                "\n【url】 ==> 【" + http.getUrl() + "】\n" +
                "【method】 ==> 【" + method + "】\n" +
                "【async】 ==> 【" + http.isAsync() + "】\n" +
                "【asyncCallback】 ==> 【" + http.getAsyncCallback() + "】\n" +
                "【isPrintLog】 ==> " + "【" + http.isPrintLog() + "】\n" +
                "【parameters】 ==> " + StrKit.toFormatString(http.getParameters(), "\t\t") + '\n' +
                "【headers】 ==> " + StrKit.toFormatString(http.getHeaders(), "\t\t") + '\n' +
                "【config】 ==> " + http.getRequestConfig() + "\n" +
                "【uploadFiles】 ==> " + StrKit.toFormatString(http.getUploadFiles(), "\t\t") + '\n' +
                "【httpResult】 ==> \n" + http.getHttpResult() +
                LOG_PRINT_BORDER;
    }


    @Note("用于给 HttpClient 发送同步 HTTPS 请求时绕过 SSL 证书校验")
    public static SSLConnectionSocketFactory private_newIgnoreVerifySSLForSyncClient() {
        try {
            TrustStrategy acceptingTrustStrategy = (chain, authType) -> true;
            SSLContext sslContext = SSLContextBuilder.create()
                    .loadTrustMaterial(null, acceptingTrustStrategy)
                    .setProtocol("TLSv1.2")
                    .build();
            return new SSLConnectionSocketFactory(
                    sslContext, new String[]{"TLSv1.2"}, null,
                    NoopHostnameVerifier.INSTANCE);
        } catch (Exception e) {
            throw new DataGenerationException(e);
        }
    }

    @Note("用于给 HttpClient 发送异步 HTTPS 请求时绕过 SSL 证书校验")
    public static SSLIOSessionStrategy private_newIgnoreVerifySSLForAsyncClient() {
        try {
            TrustStrategy acceptingTrustStrategy = (chain, authType) -> true;
            SSLContext sslContext = SSLContextBuilder.create()
                    .loadTrustMaterial(null, acceptingTrustStrategy)
                    .setProtocol("TLSv1.2")
                    .build();
            return new SSLIOSessionStrategy(
                    sslContext, new String[]{"TLSv1.2"}, null,
                    NoopHostnameVerifier.INSTANCE);
        } catch (Exception e) {
            throw new DataGenerationException(e);
        }
    }



    @Note("异步请求回调方法的私有内部类实现")
    private static class AsyncCallbackFun implements FutureCallback<HttpResponse> {

        private final HttpAsyncCallback callback;
        private final HttpBuilder httpBuilder;

        private AsyncCallbackFun(HttpAsyncCallback callback, HttpBuilder httpBuilder) {
            this.callback = callback;
            this.httpBuilder = httpBuilder;
        }

        @Override
        public void completed(HttpResponse httpResponse) {
            HttpResult httpResult = new HttpResult((CloseableHttpResponse) httpResponse);
            this.httpBuilder.setHttpResult(httpResult);
            callback.completed(httpResult);
        }

        @Override
        public void failed(Exception e) {
            callback.failed(e);
        }

        @Override
        public void cancelled() {
            callback.cancelled();
        }
    }
}
